package library

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"path"
	"path/filepath"
	"strings"
	"text/template"

	"gopkg.in/yaml.v3"
)

const (
	dockerfileOut = "Dockerfile.out"
)

type manifestGlobal struct {
	Registries []string               `yaml:"registries"`
	Dockerhub  string                 `yaml:"dockerhub"`
	Doc        string                 `yaml:"doc"`
	Upstreams  []string               `yaml:"upstreams"`
	Repos      []string               `yaml:"repos"`
	Vars       map[string]interface{} `yaml:"vars"`
	Mirrors    map[string]string      `yaml:"mirrors"`
}

type manifestRepo struct {
	Name string                            `yaml:"name"`
	Tags []manifestTag                     `yaml:"tags"`
	Vars map[string]map[string]interface{} `yaml:"vars"`
}

type manifestTag struct {
	Name       string   `yaml:"name"`
	Also       []string `yaml:"also"`
	Dockerfile string   `yaml:"dockerfile"`
	Vars       []string `yaml:"vars"`
}

type PullTask struct {
	Name string
}

type PullTaskOptions struct {
	DockerConfig string
}

func (p PullTask) Do(opts PullTaskOptions) error {
	de := DockerExecutor{DockerConfig: opts.DockerConfig}
	return de.Execute("", "pull", p.Name)
}

type BuildTask struct {
	Dir        string
	Repo       string
	Names      []string
	Doc        string
	Dockerfile string
	Vars       map[string]interface{}
}

type BuildTaskOptions struct {
	Push         bool
	DockerConfig string
}

func sanitize(buf []byte) []byte {
	lines := bytes.Split(buf, []byte{'\n'})
	out := make([][]byte, 0, len(lines))
	for _, line := range lines {
		line = bytes.TrimSpace(line)
		if len(line) == 0 {
			continue
		}
		out = append(out, line)
	}
	return bytes.Join(out, []byte{'\n'})
}

func (b BuildTask) Do(opts BuildTaskOptions) (err error) {
	de := DockerExecutor{DockerConfig: opts.DockerConfig}
	if err = ioutil.WriteFile(
		filepath.Join(b.Dir, "banner.minit.txt"),
		[]byte(fmt.Sprintf(
			"本镜像基于 %s 制作，详细信息参阅 %s",
			b.Names[0],
			strings.ReplaceAll(b.Doc, "{{.Repo}}", b.Repo),
		)),
		0644); err != nil {
		return
	}
	var buf []byte
	if buf, err = ioutil.ReadFile(filepath.Join(b.Dir, b.Dockerfile)); err != nil {
		return
	}
	var tmpl *template.Template
	if tmpl, err = template.New("__main__").Option("missingkey=zero").Parse(string(buf)); err != nil {
		return
	}
	out := &bytes.Buffer{}
	if err = tmpl.Execute(out, b.Vars); err != nil {
		return
	}
	out.WriteString("\nADD banner.minit.txt /etc/banner.minit.txt")
	if err = ioutil.WriteFile(filepath.Join(b.Dir, dockerfileOut), sanitize(out.Bytes()), 0640); err != nil {
		return
	}
	if err = de.Execute(b.Dir, "build", "-t", b.Names[0], "-f", dockerfileOut, "."); err != nil {
		return
	}
	for _, altName := range b.Names[1:] {
		if err = de.Execute(b.Dir, "tag", b.Names[0], altName); err != nil {
			return
		}
	}
	if opts.Push {
		if err = de.Execute(b.Dir, "push", b.Names[0]); err != nil {
			return
		}
		for _, altName := range b.Names[1:] {
			if err = de.Execute(b.Dir, "push", altName); err != nil {
				return
			}
		}
	}
	return
}

type MirrorTask struct {
	From string
	To   []string
}

type MirrorTaskOptions struct {
	DockerConfig string
	Tag          string
}

func (s MirrorTask) Do(opts MirrorTaskOptions) (err error) {
	de := DockerExecutor{DockerConfig: opts.DockerConfig}
	from := s.From + ":" + opts.Tag
	if err = de.Execute("", "pull", from); err != nil {
		return
	}
	for _, to := range s.To {
		to = to + ":" + opts.Tag
		if err = de.Execute("", "tag", from, to); err != nil {
			return
		}
		if err = de.Execute("", "push", to); err != nil {
			return
		}
		if err = de.Execute("", "rmi", to); err != nil {
			return
		}
	}
	if err = de.Execute("", "rmi", from); err != nil {
		return
	}
	return
}

var (
	Pulls   []PullTask
	Builds  []BuildTask
	Mirrors []MirrorTask
)

func init() {
	var err error
	defer func(err *error) {
		if *err != nil {
			panic(*err)
		}
	}(&err)
	var buf []byte
	if buf, err = ioutil.ReadFile("manifest.yml"); err != nil {
		return
	}
	var global manifestGlobal
	if err = yaml.Unmarshal(buf, &global); err != nil {
		return
	}
	// Pulls
	for _, upstream := range global.Upstreams {
		Pulls = append(Pulls, PullTask{
			Name: upstream,
		})
	}
	// Builds
	for _, dir := range global.Repos {
		dir = filepath.Join("repos", filepath.Join(strings.Split(dir, "/")...))
		var repo manifestRepo
		if buf, err = ioutil.ReadFile(filepath.Join(dir, "manifest.yml")); err != nil {
			return
		}
		if err = yaml.Unmarshal(buf, &repo); err != nil {
			return
		}
		if repo.Name == "" {
			repo.Name = filepath.Base(dir)
		}
		for _, tag := range repo.Tags {
			if tag.Dockerfile == "" {
				tag.Dockerfile = "Dockerfile"
			}
			if tag.Name == "" {
				err = fmt.Errorf("missing name for tag in repo: %s", repo.Name)
				return
			}
			vars := map[string]interface{}{
				"registry": global.Registries[0],
			}
			for k, v := range global.Vars {
				vars[k] = v
			}
			for _, kg := range tag.Vars {
				if repo.Vars[kg] == nil {
					err = fmt.Errorf("missing vars group: %s in repo: %s ", kg, repo.Name)
					return
				}
				for k, v := range repo.Vars[kg] {
					vars[k] = v
				}
			}
			var names []string
			for _, registry := range global.Registries {
				names = append(names, path.Join(registry, repo.Name+":"+tag.Name))
				for _, also := range tag.Also {
					names = append(names, path.Join(registry, repo.Name+":"+also))
				}
			}
			Builds = append(Builds, BuildTask{
				Dir:        dir,
				Names:      names,
				Repo:       repo.Name,
				Doc:        global.Doc,
				Dockerfile: tag.Dockerfile,
				Vars:       vars,
			})
		}
	}
	// Mirrors
	for from, toName := range global.Mirrors {
		task := MirrorTask{
			From: from,
		}
		for _, registry := range global.Registries {
			task.To = append(task.To, registry+"/"+toName)
		}
		task.To = append(task.To, global.Dockerhub+"/"+toName)
		Mirrors = append(Mirrors, task)
	}
}
